package mm.http;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUpload;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.servlet.ServletRequestContext;

import mm.util.ActionInvoker;
import mm.util.Constants;
import mm.util.I18NMap;
import mm.util.LOG;
import mm.util.PushableUploadListener;
import mm.util.Pusher;
import mm.util.PusherStoppedException;
import mm.util.StringUtils;
import mm.util.UploadCancelledException;
import mm.view.Page;
import mm.view.User;

public class PageDispatcher implements Filter {

	private static String contextPath=null;
	private static final String DEFAULT_ENCODING="UTF-8";

	private static final Map<String, List<ActionInvoker>> guards = new HashMap<String, List<ActionInvoker>>();
	
	private static final Map<String, ActionInvoker> actions = new HashMap<String, ActionInvoker>();

	private static int pusherIndex=0;
	private static final int MAX_PUSHER_COUNT = 50;
	private static final Map<Integer, Pusher> pushers = new HashMap<Integer, Pusher>();

	public void destroy() {
		;
	}

	public void doFilter(ServletRequest request, ServletResponse response, FilterChain clain) throws IOException, ServletException {
		HttpServletRequest req=(HttpServletRequest)request;
		HttpServletResponse res=(HttpServletResponse)response;
		if(contextPath==null)
			contextPath=req.getContextPath();
		if(!DEFAULT_ENCODING.equals(req.getCharacterEncoding()))
			req.setCharacterEncoding(DEFAULT_ENCODING);
		if(!DEFAULT_ENCODING.equals(res.getCharacterEncoding()))
			res.setCharacterEncoding(DEFAULT_ENCODING);
		String path=req.getServletPath();
		LOG.debug("path: "+path+", thread: "+Thread.currentThread().getId());
		if(path.equals("/")){
			path="/main/index";
		}
		String[] pathFrags=path.split("/");
		if(pathFrags.length<3){
			// params not enough
			clain.doFilter(req, res);
			return;
		}
		String moduleName=pathFrags[1].toLowerCase();
		String actionName=pathFrags[2].toLowerCase();
		if(actionName!=null){
			int questionIndex=actionName.indexOf("?");
			if(questionIndex>0){
				actionName=actionName.substring(0, questionIndex);
			}
		}
		if(isInvalid(moduleName)||isInvalid(actionName)){
			clain.doFilter(req, res);
			return;
		}
		User user=findCurrentUser(req);
		String defaultURL="/WEB-INF/pages/"+moduleName+"/"+actionName+".jsp";
		boolean isPosting="POST".equals(req.getMethod());
		ActionInvoker action=isPosting?
				findAction(moduleName, "do_"+actionName):
				findAction(moduleName, actionName);
		if(action==null){
			if(isPosting)
				action=findAction(moduleName, actionName);
			if(action==null){
				Page returnedPage=new Page(defaultURL);
				toPage(path, returnedPage, user, 0, req, res); // TODO security and ddos
				return;
			}
		}
		String id=null;
		String[] sequence=null;
		if(action.isNeedingId()||action.isNeedingSequence()){
			sequence=new String[]{};
			if(pathFrags.length>4){
				List<String> sequenceList=new ArrayList<String>();
				for(int i=3; i<pathFrags.length; i++){
					if(pathFrags[i].length()>0){
						sequenceList.add(pathFrags[i]);
					}
				}
				if(sequenceList.size()>1){
					sequence=sequenceList.toArray(sequence); // toArray uses like this?
				}else if(sequenceList.size()>0){
					id=sequenceList.get(0);
				}
			}else if(pathFrags.length==4){
				id=pathFrags[3];
			}
		}
		Map<String, Object> params=null;
		Pusher pusher=null;
		ServletRequestContext requestContext=new ServletRequestContext(req);
		boolean isMulti=FileUpload.isMultipartContent(requestContext);
		if(!isMulti)
			params=getParamMap(req);
		if(action.isNeedingPusher()){
			String query=req.getQueryString();
			LOG.debug(query);
			String pusherId = isMulti?readPusherIdFromQuery(query):(String)params.get("pusher");
			if(pusherId!=null&&pusherId.length()>0){
				pusher = findPusher(pusherId);
			}
			if(pusher==null){
				pusher=new Pusher(res);
				int existingPusherCount=pushers.size();
				if(existingPusherCount>=MAX_PUSHER_COUNT){
					pusher.setId(new Integer(777)); // id 777 is no use
					pusher.error(new RuntimeException("Max long connection size reached, Please try again somewhile."));
					pusher.done();
					return;
				}
				Integer newPusherIndex=new Integer(pusherIndex++);
				pusher.setId(newPusherIndex);
				pushers.put(newPusherIndex, pusher);
			}
		}
		List<ActionInvoker> guards=findGuards(moduleName);
		if(guards!=null)
			for(ActionInvoker guard : guards){
				Exception e=null;
				try {
					e = (Exception) guard.invoke(user, id, sequence, params, null); // this 'param' only from non-multi request
				} catch (Exception e1) {
					e=e1;
				}
				if(e!=null){
					warn(res, "leave away! "+e.getMessage()); // FIXME
					return;
				}
			}
		Class returnType = action.getReturnType();
		if(action.isNeedingParamMap()&&isMulti){
			FileUpload fileUpload=new ServletFileUpload(new DiskFileItemFactory());
			PushableUploadListener listener = action.getUploadListener();
			if(listener!=null){
				fileUpload.setProgressListener(listener);
				if(pusher!=null){
					pusher.setUploadStats(listener.getUploadStats());
					listener.setPusher(pusher);
				}
			}
			params=new HashMap<String, Object>();
			List items = null;
			try {
				items = fileUpload.parseRequest(requestContext);
			} catch (Exception e) {
				LOG.error(e.getMessage(), e);
				if(pusher!=null&&pusher.isAvailable()){
					pusher.error(e);
					pusher.done();
					pushers.remove(pusher.getId());
				}else{
					write(res, e.getMessage()); // TODO does this own?
				}
				return;
			}
			for (Iterator i = items.iterator(); i.hasNext();) {
				FileItem fileItem = (FileItem) i.next();
				if (fileItem.isFormField()) {
					params.put(fileItem.getFieldName(), fileItem.getString());
				}else{
					params.put(fileItem.getFieldName(), fileItem);
				}
			}
		}
		Object returnedObject=null;
		long start=System.currentTimeMillis();
		long duringTime=0;
		try{
			returnedObject=action.invoke(user, id, sequence, params, pusher);
		}catch(Throwable t){
			Throwable cause = t.getCause();
			if(cause==null)
				cause=t;
			if(!(cause instanceof PusherStoppedException)){
				LOG.error(cause.getMessage(), cause);
			}
			if(pusher!=null){
				if(cause instanceof PusherStoppedException){
					// TODO stopped. do nothing now. may add some message from ex?
					pushers.remove(pusher.getId());
					return;
				}
				pusher.error(cause);
				pushers.remove(pusher.getId());
				return;
			}
			if(returnType.equals(String.class)){ // ajax
				write(res, "{error:\""+I18NMap.getInstance(user.getLocale()).get(cause.getMessage())+"\"}");
			}else{
				warn(res, "500 inner error");
			}
			return;
		}finally{
			duringTime=System.currentTimeMillis()-start;
			duringTime=duringTime/1000;
			LOG.debug("Requesting '"+path+"' costed "+duringTime+" ms");
			if(pusher!=null){
				if(pusher.isAvailable())
					pusher.done();
				pushers.remove(pusher.getId());
				// Ingore returned object if pusher exists
				pusher=null;
				return;
			}
		}
		if(returnType.equals(Void.TYPE)){
			Page returnedPage=new Page(defaultURL);
			toPage(path, returnedPage, user, duringTime, req, res);
		}else if(returnType.equals(String.class)){
			String returnedContent=(String)returnedObject;
			write(res, returnedContent);
		}else if(returnType.equals(Page.class)){
			Page returnedPage=(Page)returnedObject;
			String url=returnedPage!=null&&(url=returnedPage.getURL())!=null?url:defaultURL;
			returnedPage.setURL(url);
			toPage(path, returnedPage, user, duringTime, req, res);
		}
	}

	private String readPusherIdFromQuery(String query) {
		if(query==null){
			return null;
		}
		String pusherQuery="pusher=";
		int index=query.indexOf(pusherQuery);
		if(index<0){
			return null;
		}
		query=query.substring(index+pusherQuery.length());
		query=query.split("&")[0];
		return query;
	}

	private void toPage(String path, Page page, User user, long duringTime, HttpServletRequest req, HttpServletResponse res) {
		String method=req.getMethod();
		if("GET".equals(method)){
			prepareAttributes(path, page, user, duringTime, req);
			forwardTo(req, res, page.getURL());
		}else{
			// all treat as POST
			redirectTo(res, page.getURL());
		}
	}

	private void prepareAttributes(String path, Page page, User user, long duringTime, HttpServletRequest req) {
		if(page==null){
			return;
		}
		Map<String, Object> pageBeans=page.getBeans();
		pageBeans.put("basePath", contextPath);
		pageBeans.put("url", path);
		pageBeans.put("user", user);
		pageBeans.put("i18n", I18NMap.getInstance(user.getLocale()));
		pageBeans.put("time", duringTime);
		for(String beanName : pageBeans.keySet()){
			Object bean=pageBeans.get(beanName);
			if(bean!=null){
				req.setAttribute(beanName, bean);
			}
		}
		if(page.isNoFrame()){
			req.setAttribute("noFrame", true);
		}
	}

	private List<ActionInvoker> findGuards(String moduleName) {
		return guards.get(moduleName);
	}

	private boolean isInvalid(String moduleOrActionName) {
		if(moduleOrActionName==null||moduleOrActionName.trim().length()<1)
			return true;
		if(moduleOrActionName.indexOf(".")>-1)
			return true;
		if(!moduleOrActionName.matches("[a-z0-9_]+"))
			return true;
		return false;
	}

	private void warn(HttpServletResponse res, String msg) {
		// TODO ajax warning
		redirectTo(res, "/main/warn/"+msg); // FIXME use error code instead of msg
	}

	private void redirectTo(HttpServletResponse res, String url) {
		try {
			res.sendRedirect(contextPath+url);
		} catch (IOException e) {
			LOG.error(e);
			warn(res, "500 inner error");
		}
	}

	private void forwardTo(HttpServletRequest req, HttpServletResponse res, String url) {
		if(req.getAttribute("noFrame")==null){
			req.setAttribute("url", url);
			url="/WEB-INF/pages/frame.jsp";
		}
		RequestDispatcher requestDispatcher = req.getRequestDispatcher(url);
		if(requestDispatcher!=null){
			try {
				requestDispatcher.forward(req, res);
			} catch (Exception e) {
				LOG.error("Error in forwarding page:", e);
				warn(res, "500 inner error");
			}
		}else{
			warn(res, "404 not found");
		}
	}

	private ActionInvoker findAction(String moduleName, String actionName) {
		return actions.get(moduleName+"."+actionName);
	}

	private User findCurrentUser(HttpServletRequest req) {
		User user=(User) req.getSession(true).getAttribute("user");
		if(user==null){
			user=new User();
			req.getSession(true).setAttribute("user", user);
			Locale locale = req.getLocale();
			user.setRequestLocale(user.getLocale());
			if(Constants.LOCALES.containsBar(locale)){
				user.setLocale(locale);
			}else{
				user.setLocale(Constants.DEFAULT_LOCALE);
			}
		}
		Locale requestLocale = req.getLocale();
		if(!requestLocale.equals(user.getRequestLocale())){
			user.setRequestLocale(requestLocale);
			if(Constants.LOCALES.containsBar(requestLocale)){
				user.setLocale(requestLocale);
			}
		}
		return user;
	}

	@SuppressWarnings("unchecked")
	public static Map<String, Object> getParamMap(ServletRequest req) {
		Map<String, Object> params=new HashMap<String, Object>();
		Map<String, String[]> map=req.getParameterMap();
		for(String name:map.keySet()){
			// FIXME only pass single-value field
			String value=map.get(name)[0];
			if(value!=null&&value.length()>0)
				params.put(name, value);
		}
		return params;
	}

	public void init(FilterConfig config) throws ServletException {
		try {
			// removed for below-Servlet-spec-v2.5 container
//			try{
//				contextPath=config.getServletContext().getContextPath();
//			}catch(NoSuchMethodError e){
//				LOG.warn("Found a below-Servlet-v2.5 container, using HttpServletRequest.getContextPath() instead of ServletContext.getContextPath().");
//			}
			String controllerPackageName=this.getClass().getPackage().getName();
			controllerPackageName=controllerPackageName.substring(0, controllerPackageName.lastIndexOf(".")+1)+"controller";
			List<File> directories = new ArrayList<File>();
	        ClassLoader cld = Thread.currentThread().getContextClassLoader();
            if (cld == null) {
                LOG.error("Can't get class loader.");
                return;
            }
            String path = controllerPackageName.replace('.', '/');
            // Ask for all resources for the path
            Enumeration<URL> resources = cld.getResources(path); // TODO learn why path duped
            while (resources.hasMoreElements()) {
                directories.add(new File(URLDecoder.decode(resources.nextElement().getPath(), "UTF-8")));
            }
            Set<String> usedDirs=new HashSet<String>();
            // For every directory identified capture all the .class files
            for (File directory : directories) {
            	if(usedDirs.contains(directory.getAbsolutePath())){
            		continue;
            	}
            	usedDirs.add(directory.getAbsolutePath());
                if (directory.exists()) {
                    // Get the list of the files contained in the package
                    String[] modules = directory.list();
                    for (String module : modules) {
    					// we are only interested in .class files
    					if (module.endsWith(".class")) {
    						if (module.indexOf("Test")>-1) {
    							LOG.debug("Ignoring test case: "+module);
    							continue;
    						}
    						// removes the .class extension
    						Class clazz = null;
    						try {
    							String moduleClassName=module.substring(0, module.length() - 6);
    							clazz = Class.forName(controllerPackageName + '.'
    									+ moduleClassName);
    							if (clazz.getModifiers() != Modifier.PUBLIC
    									|| clazz.getConstructor(new Class[] {}) == null
    									|| clazz.getConstructor(new Class[] {})
    											.isAccessible()) {
    								LOG.debug("class '" + clazz
    										+ "' can't be instanced");
    								continue;
    							}
    							String moduleName=StringUtils.lowerCase(moduleClassName);
    							Object object = clazz.newInstance();
    							Method[] methods = clazz.getMethods();
    							outter: 
    							for (Method method : methods) {
    								if (method.getModifiers() != Modifier.PUBLIC) {
    									continue;
    								}
    								if (method.getName().startsWith("_") // service
    										|| method.getName().startsWith("$")) { // persistence
    									continue;
    								}
    								if (method.getName().equals("toString")) {
    									continue;
    								}
    								Class[] paramClasses = method
    										.getParameterTypes();
    								if (paramClasses != null
    										&& paramClasses.length > 0) {
    									for (Class paramClass : paramClasses) {
    										if (!paramClass.equals(User.class)
    												&& !paramClass
    														.equals(String[].class)
    												&& !paramClass
    														.equals(String.class)
    												&& !paramClass
    														.equals(Map.class)
    												&& !paramClass
    														.equals(Pusher.class)) {
    											continue outter;
    										}
    									}
    								}
    								String actionName = StringUtils
    										.lowerCase(method.getName());
    								ActionInvoker actionInvoker = new ActionInvoker(
    										method, object);
    								Class returnClass = method.getReturnType();
    								if (!returnClass.equals(Void.TYPE)
    										&& !returnClass.equals(String.class)
    										&& !returnClass.equals(Page.class)
    										&& !isException(returnClass)) {
    									continue outter;
    								}
    								if (isException(returnClass)) {
    									List<ActionInvoker> guardList = guards
    											.get(moduleName);
    									if (guardList == null) {
    										guardList = new ArrayList<ActionInvoker>();
    										guards.put(moduleName, guardList);
    									}
    									guardList.add(actionInvoker); // FIXME using default order now
    								} else {
    									actions.put(moduleName + "."
    											+ actionName, actionInvoker); // No overloading now
    								}
    							}
    						} catch (Exception e) {
    							LOG.error("class '" + clazz
    									+ "' can't be instanced", e);
    							continue;
    						}
    					}
    				}
                } else {
                	LOG.error(controllerPackageName + " (" + directory.getPath() + ") does not appear to be a valid package");
                    continue;
                }
            }
//			if(SystemListener.isDebugging()){
//				LOG.debug("Guards:\r\n"+StringUtils.toString(guards));
//				LOG.debug("Actions:\r\n"+StringUtils.toString(actions));			
//			}
            prepareContextAttrbutes(config.getServletContext());
        } catch (Throwable t) {
        	LOG.error("PageDispatcher init error:", t);
            return;
        }
	}

	@SuppressWarnings("unchecked")
	private boolean isException(Class clazz) {
		if(clazz==null){
			return false;
		}
		try{
			clazz.asSubclass(Exception.class);
		}catch(ClassCastException e){
			return false;
		}
		return true;
	}

	private void write(ServletResponse res, String content) throws IOException {
		PrintWriter out = res.getWriter();
		out.print(content!=null?content:"");
		out.flush();
	}

	private void prepareContextAttrbutes(ServletContext servletContext) {
		servletContext.setAttribute("locales", Constants.LOCALES.barMap().keySet());
	}

	public static Pusher findPusher(Integer pusherId) {
		return pushers.get(pusherId);
	}

	public static Pusher findPusher(String pusherIdS) {
		if(pusherIdS==null||!pusherIdS.matches("\\d+")){
			return null;
		}
		Integer pusherId=null;
		try{
			pusherId=Integer.valueOf(pusherIdS);
		}catch(Exception e){
			return null;
		}
		return pushers.get(pusherId);
	}
	
}
